#!/usr/bin/env node
// 参考: https://pan.baidu.com/union/doc

const os= require('os');
const fs = require('fs-extra');
const npath = require('path');
const moment = require('moment');
const superagent = require('superagent');
const command = require('./js/command');
const utils = require('./js/utils');
const _ = require('lodash');
const program = require('commander');
const crypto = require('crypto');
const shell = require('shelljs');
const configFile = `${os.homedir()}/.wangpan_config`;
const CACHE = {};
const FS = []; // { path, isdir, name, ... }
let cmd;
let debug = false;
const BUFFER_SIZE = 4 * 1024 * 1024;
let _appkey = 'ANDNo2UCrAuUyqSjoydwuDcIrF1wH190';
let _secretkey = '0CwSpLeqbWQtm1Bx6NGGcOXW1hteDkji';
let _root = '/apps/fangyunjiang';
let _user = 'fang';
let _pwd = '/';

function path_join(...items) {
    return items.join('/');
}
function path_resolve(...items) {
    return npath.resolve(...items).replace(/^[Cc]:/, '').replace(/\\/g, '/');
}
function getMd5(text) {
    const hash = crypto.createHash('md5');
    hash.update(text);
    return hash.digest('hex');
}
function streamToBuffer (stream, encoding) {
    return new Promise(resolve => {
        const buffers = [];
        stream.on('error', () => resolve(null));
        stream.on('data', (data) => buffers.push(data));
        stream.on('end', () => resolve(!encoding ? Buffer.concat(buffers) : Buffer.concat(buffers).toString(encoding)));
    });
}
function baiduGet (url, data, text) {
    debug && console.log('[baiduGet] send:', url, data);
    return new Promise(resolve => {
        superagent.get(url)
        .set({ 'Content-Type': `application/x-www-form-urlencoded;charset=UTF-8` })
        .set({ 'User-Agent': 'pan.baidu.com' })
        .query(data)
        .end((error, res) => {
            if (!error && res.statusCode === 200) {
                if (text) {
                    if (res.text) {
                        debug && console.log('[baiduGet] receive text:', url, res.data);
                        return resolve(res.text);
                    } else {
                        return streamToBuffer(res, 'utf-8').then(text=>{
                            debug && console.log('[baiduGet] receive text:', url, text);
                            resolve(text);
                        });
                    }
                }
                const obj = utils.parseJSON(res.text);
                debug && console.log('[baiduGet] receive:', url, obj);
                resolve(obj);
            } else {
                debug && console.log('[baiduGet] error:', res.body);
                resolve({ error: res.body.error_description });
            }
        });
    });
}
function baiduPost (url, data) {
    debug && console.log('[baiduPost] send:', url, data);
    return new Promise(resolve => {
        superagent.post(url)
        .set({ 'Content-Type': `application/x-www-form-urlencoded;charset=UTF-8` })
        .set({ 'User-Agent': 'pan.baidu.com' })
        .send(data)
        .end((error, res) => {
            if (!error && res.statusCode === 200) {
                const obj = utils.parseJSON(res.text);
                debug && console.log('[baiduPost] receive:', url, obj);
                resolve(obj);
            } else {
                debug && console.log('[baiduPost] error:', res.body);
                resolve({ error: res.body.error_description });
            }
        });
    });
}
async function precreateFile (path, size, block_list) {
    return await baiduPost(`https://pan.baidu.com/rest/2.0/xpan/file?method=precreate&access_token=${options.access_token}`, {
        path, size, rtype: '0', isdir: '0', autoinit: 1, block_list,
    });
}
function uploadFile (uploadid, buffer, name, path, partseq=0) {
    return new Promise(resolve => {
        const url = `https://d.pcs.baidu.com/rest/2.0/pcs/superfile2?method=upload&access_token=${options.access_token}&path=${encodeURI(path)}&type=tmpfile&uploadid=${uploadid}&partseq=${partseq}`;
        debug && console.log('[uploadFile] send:', path, url);
        debug && console.log('[uploadFile] send:', path, buffer);
        superagent.post(url)
        .set({ 'User-Agent': 'pan.baidu.com' })
        .attach('file', buffer, name)
        .end((error, res) => {
            if (error) {
                debug && console.log("[uploadFile] error: ", error);
                return resolve({ error: 'upload失败' });
            }
            debug && console.log('[uploadFile] recv:', res.text);
            resolve({});
        });
    });
}
async function createFile (path, size, uploadid, block_list) {
    return await baiduPost(`https://pan.baidu.com/rest/2.0/xpan/file?method=create&access_token=${options.access_token}`, {
        path, size: size, rtype: '0', isdir: '0', uploadid, block_list,
    });
}
async function writeFile (path, text) {
    const size = Buffer.byteLength(text);
    const md5 = await getMd5(text);
    const block_list = JSON.stringify([md5]);
    let data = await precreateFile(path, size, block_list);
    if (data.error || data.errno) {
        return cmd.error('precreateFile失败');
    }
    const uploadid = data.uploadid;
    data = await uploadFile(uploadid,  Buffer.from(text), npath.basename(path), path);
    if (data.error) {
        return cmd.error(data.error);
    }
    data = await createFile(path, size, uploadid, block_list);
    if (data.error || data.errno) {
        return cmd.error('createFile失败');
    }
    cmd.prompt();
}
async function pushFile (path, localPath) {
    let fd = fs.openSync(localPath);
    const size = fs.fstatSync(fd).size;
    let buffer = Buffer.alloc(BUFFER_SIZE);
    let block_list = [];
    let pos = 0;
    while (pos < size) {
        fs.readSync(fd, buffer, 0, BUFFER_SIZE, pos);
        if (size - pos < BUFFER_SIZE) {
            buffer = buffer.slice(0, size - pos);
        }
        block_list.push(getMd5(buffer));
        pos+= BUFFER_SIZE;
        buffer.fill(0);
    }
    fs.closeSync(fd);
    block_list = JSON.stringify(block_list);
    let data = await precreateFile(path, size, block_list);
    if (data.error || data.errno) {
        return cmd.error('precreateFile失败');
    }
    const uploadid = data.uploadid;
    fd = fs.openSync(localPath);
    pos = 0;
    buffer = Buffer.alloc(BUFFER_SIZE);
    let i = 0;
    while (pos < size) {
        fs.readSync(fd, buffer, 0, BUFFER_SIZE, pos);
        if (size - pos < BUFFER_SIZE) {
            buffer = buffer.slice(0, size - pos);
        }
        data = await uploadFile(uploadid, buffer, npath.basename(path), path, i);
        if (data.error) {
            fs.closeSync(fd);
            return cmd.error(data.error);
        }
        pos+= BUFFER_SIZE;
        buffer.fill(0);
        i++;
    }
    fs.closeSync(fd);

    data = await createFile(path, size, uploadid, block_list);
    if (data.error || data.errno) {
        return cmd.error('createFile失败');
    }
    cmd.prompt();
}
async function getFileText (path, fs_id) {
    if (!fs_id) {
        const file = await getFsFile(path);
        if (!file) {
            return cmd.error('没有该文件');
        }
        fs_id = file.fs_id;
    }
    const data = await baiduGet(`https://pan.baidu.com/rest/2.0/xpan/multimedia?method=filemetas&fsids=[${fs_id}]&access_token=${options.access_token}&dlink=1`);
    if (data.error) {
        return cmd.error('获取文件信息失败:' + data.error);
    }
    const dlink = _.get(data, 'list.0.dlink');
    if (!dlink) {
        return cmd.error('文件信息错误');
    }
    return await baiduGet(`${dlink}&access_token=${options.access_token}`, {}, true);
}
async function download (url, destPath) {
    debug && console.log('[download]:', url, destPath);
    return new Promise(resolve => {
        superagent.get(url)
        .set({ 'Content-Type': `application/x-www-form-urlencoded;charset=UTF-8` })
        .responseType('arraybuffer')
        .on('end', () => {
            resolve();
        })
        .pipe(fs.createWriteStream(destPath));
    });
}
async function downloadFile (path, fs_id, destPath) {
    if (!fs_id) {
        const file = await getFsFile(path);
        if (!file) {
            return cmd.error('没有该文件');
        }
        fs_id = file.fs_id;
    }
    const data = await baiduGet(`https://pan.baidu.com/rest/2.0/xpan/multimedia?method=filemetas&fsids=[${fs_id}]&access_token=${options.access_token}&dlink=1`);
    if (data.error) {
        return cmd.error('获取文件信息失败:' + data.error);
    }
    const dlink = _.get(data, 'list.0.dlink');
    if (!dlink) {
        return cmd.error('文件信息错误');
    }
    await download(`${dlink}&access_token=${options.access_token}`, destPath);
    cmd.success('下载完成');
}
function createWangpan (commands, options) {
    // 自动完成
    async function completer(line, callback) {
        const words = line.trim().split(/\s/);
        let list = [];
        if (words.length <= 1) {
            const cmds = [ 'help', 'cls', 'pwd', 'whoami', 'ls', 'll', 'mkdir', 'cd', 'mv', 'rm', 'touch', 'echo', 'cat', 'pull', 'push', 'xcat', 'lg' ];
            const _linePartial = line.trim();
            list = cmds.filter((o) => o.startsWith(_linePartial));
            return callback(null, [list, _linePartial]);
        }
        const lastWord = _.last(words);
        const items = lastWord.split('/');
        const linePartial = _.last(items);
        const prepath = items.slice(0, -1).join('/');
        const firstWord = words[0];
        if (_.includes(['mv', 'rm', 'cat', 'pull', 'push', 'xcat', 'lg'], firstWord)) {
            list = await getAllFiles(prepath);
        } else if (_.includes(['ls', 'll', 'mkdir', 'cd', 'touch', 'echo'], firstWord)) {
            list = await getDirs(prepath);
        }
        const found = list.filter((o) => o.startsWith(linePartial));
        return callback(null, [found.length ? found : list, linePartial]);
    }
    CACHE._options = { ...options, completer };
    cmd = command(commands, CACHE._options);
}
function getSize(length) {
    let unit = 'B';
    if (length > 1024) {
        length = (length / 1024).toFixed(2);
        unit = 'K';
        if (length > 1024) {
            length = (length / 1024).toFixed(2);
            unit = 'M';
            if (length > 1024) {
                length = (length / 1024).toFixed(2);
                unit = 'G';
            }
        }
    }
    return length + unit;
}
function getAllFiles(path) {
    path = getPath(path);
    return _.filter(FS, o => npath.dirname(o.path)===path).map(o => o.server_filename);
}
function getDirs(path) {
    path = getPath(path);
    return _.filter(FS, o => npath.dirname(o.path)===path && o.isdir).map(o => o.server_filename);
}
function getPath(line) {
    const path = /^\//.test(line) ? line : /^~/.test(line) ? line.replace(/^~/, _root) : `${_root}${_pwd}/${line}`;
    return path_resolve(path.replace(/\/*$/, ''));
}
function checkLogin(callback) {
    return function(...params) {
        if (!options.access_token) {
            return this.error('请先登录');
        }
        callback.call(cmd, ...params);
    }
}
async function login(code) {
    if (!code) {
        this.print(`将下面的url复制到浏览器访问，然后使用返回的code进行登录: login code`, 'gray');
        this.print(`http://openapi.baidu.com/oauth/2.0/authorize?response_type=code&client_id=${_appkey}&scope=basic,netdisk&redirect_uri=oob&qrcode=1`, 'green');
        return this.prompt();
    }
    const data = await baiduGet(`https://openapi.baidu.com/oauth/2.0/token?grant_type=authorization_code&code=${code}&client_id=${_appkey}&client_secret=${_secretkey}&redirect_uri=oob`);
    if (data.error) {
        return this.error('登录失败:' + data.error);
    }
    options.access_token = data.access_token;
    options.refresh_token = data.refresh_token;
    options.deadTime = moment().add(data.expires_in - 7200, 's').format('YYYY-MM-DD HH:mm:ss');
    fs.writeJSONSync(configFile, options);
    await doMkdir(_root);
    this.prompt();
}
function pwd(line = '') {
    _pwd=path_join(_pwd, line);
    this.print(`${_pwd}`, 'gray');
    this.prompt();
}
async function whoami(line) {
    const data = await baiduGet(`https://pan.baidu.com/rest/2.0/xpan/nas?method=uinfo&access_token=${options.access_token}`);
    if (data.error) {
        return this.error('获取个人信息失败:' + data.error);
    }
    this.print(`${data.baidu_name}`, 'gray');
    this.prompt();
}
function cd(line = '') {
    if (!line) {
        _pwd = '/';
    } else {
        _pwd=path_join(_pwd, line);
    }
    this.setPrompt(`wangpan@${_user}:${_pwd} $\n`);
    this.prompt();
}
async function getFsFile(path) {
    const item = _.find(FS, o=>o.path === path);
    if (item) {
        return item;
    }
    const data = await baiduGet(`https://pan.baidu.com/rest/2.0/xpan/file?method=list&access_token=${options.access_token}&dir=${npath.dirname(path)}&folder=0&showempty=1&start=0&limit=100`);
    const list = _.get(data, 'list', []);
    refreshFS(list);
    return _.find(list, o=>o.path === path);
}
function refreshFS(list) {
    for (const file of list) {
        const item = _.find(FS, o=>o.path === file.path);
        if (!item) {
            FS.push(file);
        } else {
            Object.assign(item, file);
        }
    }
}
async function pcls(line = '') {
    let dir = line || '.';
    const folderList = [];
    const fileList = [];
    fs.readdirSync(dir).forEach((file, index) => {
        var fullname = path_join(dir, file);
        var info = fs.statSync(fullname);
        if(info.isDirectory()) {
            folderList.push({ name: file, isdir: true });
        } else {
            fileList.push({ name: file });
        }
    });
    this.printArray([ ...folderList, ...fileList ].map(o=>({msg: o.name+'  ', color: o.isdir ? 'blue' : 'green' })));
    this.prompt();
}
async function ls(line = '') {
    const path = getPath(line);
    const data = await baiduGet(`https://pan.baidu.com/rest/2.0/xpan/file?method=list&access_token=${options.access_token}&dir=${path}&folder=0&showempty=1&start=0&limit=100`);
    const list = _.get(data, 'list', []);
    refreshFS(list);
    const folderList = _.filter(list, o => o.isdir).map(o => ({ isdir: 1, name: o.server_filename }));
    const fileList = _.filter(list, o => !o.isdir).map(o => ({ name: o.server_filename }));
    this.printArray([ ...folderList, ...fileList ].map(o=>({msg: o.name+'  ', color: o.isdir ? 'blue' : 'green' })));
    this.prompt();
}
async function ll(line = '') {
    const path = getPath(line);
    const data = await baiduGet(`https://pan.baidu.com/rest/2.0/xpan/file?method=list&access_token=${options.access_token}&dir=${path}&folder=0&showempty=1&start=0&limit=100`);
    const list = _.get(data, 'list', []);
    refreshFS(list);
    const folderList = _.filter(list, o => o.isdir).map(o => ({
        isdir: 1,
        name: o.server_filename
    }));
    const fileList = _.filter(list, o => !o.isdir).map(o => ({
        name: o.server_filename,
        md5: o.md5,
        category: o.category,
        size: o.size,
        server_ctime: o.server_ctime,
        server_mtime: o.server_mtime,
    }));

    let texts = [];
    texts[0] = { msg: _.padEnd('name', 40), color: 'black' };
    texts[1] = { msg: _.padEnd('category', 20), color: 'black' };
    texts[2] = { msg: _.padEnd('size', 20), color: 'black' };
    texts[3] = { msg: _.padEnd('createTime', 25), color: 'black' };
    texts[4] = { msg: _.padEnd('modifyTime', 25), color: 'black' };
    texts[5] = { msg: 'md5', color: 'black' };
    this.printArray(texts);
    for (const file of [ ...folderList, ...fileList ]) {
        texts = [];
        texts[0] = { msg: _.padEnd(file.name, 40), color: file.isdir ? 'blue' : 'green' };
        texts[1] = { msg:file.isdir ? '' : _.padEnd(file.category, 20), color: 'gray' };
        texts[2] = { msg: file.isdir ? '' : _.padEnd(getSize(file.size), 20), color: 'gray' };
        texts[3] = { msg: _.padEnd(moment(file.server_ctime).format('YYYY-MM-DD HH:mm:ss'), 25), color: 'gray' };
        texts[4] = { msg: _.padEnd(moment(file.server_mtime).format('YYYY-MM-DD HH:mm:ss'), 25), color: 'gray' };
        texts[5] = { msg: file.md5 || '', color: 'gray' };
        this.printArray(texts);
    }
    this.prompt();
}
async function doMkdir(path) {
    return await baiduPost(`https://pan.baidu.com/rest/2.0/xpan/file?method=create&access_token=${options.access_token}`, {
        path, size: '0', rtype: '0', isdir: '1', block_list: '[]',
    });
}
async function mkdir(line = '') {
    const data = await doMkdir(getPath(line));
    if (data.error) {
        return this.error('创建文件夹失败:' + data.error);
    }
    this.prompt();
}
async function mv(line = '') {
    if (!line) {
        return this.error('no file');
    }
    const files = line.split(/\s+/).filter(Boolean).map(o=>getPath(o));
    let data;
    if (npath.dirname(files[0]) === npath.dirname(files[1])) {
        data = await baiduPost(`https://pan.baidu.com/rest/2.0/xpan/file?method=filemanager&opera=rename&access_token=${options.access_token}`, {
            async: 0, filelist: JSON.stringify([{path: files[0], newname: npath.basename(files[1]) }]), ondup: 'fail',
        });
    } else {
        data = await baiduPost(`https://pan.baidu.com/rest/2.0/xpan/file?method=filemanager&opera=move&access_token=${options.access_token}`, {
            async: 0, filelist: JSON.stringify([{path: files[0], dest: npath.dirname(files[1]), newname: npath.basename(files[1]) }]), ondup: 'fail',
        });
    }
    if (data.error) {
        return this.error('删除失败:' + data.error);
    }
    this.prompt();
}
async function rm(line = '') {
    if (!line) {
        return this.error('no file');
    }
    const files = line.replace(/\\ /g, '#####').split(/\s+/).map(o=>o.replace('#####', ' ')).filter(Boolean).map(o=>getPath(o));
    const data = await baiduPost(`https://pan.baidu.com/rest/2.0/xpan/file?method=filemanager&opera=delete&access_token=${options.access_token}`, {
        async: 0, filelist: JSON.stringify(files), ondup: 'fail',
    });
    if (data.error) {
        return this.error('删除失败:' + data.error);
    }
    this.prompt();
}
async function touch(line = '') {
    await writeFile(getPath(line), '')
}
async function echo(line = '') {
    const matches = line.match(/(.*[^>])(>{1,2})\s*([.\w]+)$/);
    if (matches) {
        const text = matches[1];
        const op = matches[2];
        const file = matches[3];
        const path = getPath(file);
        if (op === '>') {
            await writeFile(path, text);
        } else {
            const file = await getFsFile(path);
            if (!file) {
                await writeFile(path, text);
            } else {
                const fileContent = await getFileText(path, file.fs_id);
                const data = await baiduPost(`https://pan.baidu.com/rest/2.0/xpan/file?method=filemanager&opera=delete&access_token=${options.access_token}`, {
                    async: 0, filelist: JSON.stringify([path]), ondup: 'fail',
                });
                if (data.error) {
                    return this.error('删除失败:' + data.error);
                }
                await writeFile(path, fileContent+'\n'+text);
            }
        }
    } else {
        this.print(`${line}`, 'gray');
        this.prompt();
    }
}
function lg(line = '') {
    if (!line) {
        return this.error('no file');
    }
    utils.paste(async (text)=>{
        await writeFile(getPath(line), text);
    });
}
async function cat(line = '') {
    if (!line) {
        return this.error('no file');
    }
    const fileContent = await getFileText(getPath(line));
    this.print(fileContent, 'gray');
    this.prompt();
}
async function xcat(line = '') {
    if (!line) {
        return this.error('no file');
    }
    const fileContent = await getFileText(getPath(line));
    utils.copy(fileContent);
    this.prompt();
}
async function pull(line = '') {
    if (!line) {
        return this.error('no file');
    }
    let wangpan_remote, pc_local;
    if (/.*\s+.*/.test(line)) {
        const list = line.split(/\s+/);
        wangpan_remote = list[0];
        pc_local = list[1];
    } else {
        wangpan_remote = line;
        const list = line.split('/');
        pc_local = _.last(list);
    }
    if(fs.existsSync(pc_local)) {
        if (fs.statSync(pc_local).isDirectory()) {
            pc_local = path_join(pc_local, npath.basename(wangpan_remote));
            if(fs.existsSync(pc_local)) {
                return this.error(`${dist} exists, cannot push`);
            }
        } else {
            return this.error(`${pc_local} exists, cannot push`);
        }
    }
    await downloadFile(getPath(wangpan_remote), undefined, pc_local);
}
async function push(line = '') {
    if (!line) {
        return this.error('no file');
    }
    let wangpan_remote, pc_local;
    if (/.*\s+.*/.test(line)) {
        const list = line.split(/\s+/);
        pc_local = list[0];
        wangpan_remote = list[1];
    } else {
        pc_local = line;
        wangpan_remote =  getPath(_.last(line.split('/')));
    }
    await pushFile(wangpan_remote, pc_local);
}
function changeDebug () {
  debug = !debug;
  this.success(`debug set to ${debug}`);
}
function showHelp () {
    this.print('commands:', 'blue');
    this.print('    h: show help');
    this.print('    help: show full help');
    this.print('    <q|exit>: exit');
    this.print('    login whoami pwd cd ls ll mkdir mv rm touch echo cat pull push xcat lg debug');
    this.print('    pull wangpan_remote[ pc_local] (xx/yy->./yy)');
    this.print('    push pc_local[ wangpan_remote] (xx/yy->/yy)');
    this.print('');
    this.prompt();
}
const COMMANDS = {
    'h': showHelp,
    'help': showHelp,
    'q|exit': function () { this.close() },
    'cls': function() { this.clear() },
    'login': login,
    'pcls': pcls,
    'pwd': checkLogin(pwd),
    'whoami': checkLogin(whoami),
    'ls': checkLogin(ls),
    'll': checkLogin(ll),
    'mkdir': checkLogin(mkdir),
    'cd': checkLogin(cd),
    'mv': checkLogin(mv),
    'rm': checkLogin(rm),
    'touch': checkLogin(touch),
    'echo': checkLogin(echo),
    'cat': checkLogin(cat),
    'pull': checkLogin(pull),
    'push': checkLogin(push),
    'xcat': checkLogin(xcat),
    'lg': checkLogin(lg),
    'debug': changeDebug,
};


program
.version('1.0.0')
.option('-a, --appkey [appkey]', 'set appkey', '')
.option('-s, --secretkey [secretkey]', 'set secretkey', '')
.option('-u, --user [user]', 'set user', '')
.option('-c, --config', 'show config', '')
.parse(process.argv);

let options = {};
try {
    options = fs.readJSONSync(configFile);
} catch (e) {}
_appkey = program.appkey || options.appkey || _appkey;
_secretkey = program.secretkey || options.secretkey || _secretkey;
_user = program.user || options.user || _user;
if (program.config) {
    console.log({ appkey: _appkey, secretkey: _secretkey, user: _user });
    process.exit(0);
}
if (options.appkey !== _appkey || options.secretkey !== _secretkey || options.user !== _user) {
    fs.writeJSONSync(configFile, { appkey: _appkey, secretkey: _secretkey, user: _user });
}
createWangpan(COMMANDS, { title: 'wangpan', prompt:`wangpan@${_user}:${_pwd} $\n`, history: [ 'h', 'help', 'cls', 'pwd', 'whoami', 'pcls', 'ls', 'll', 'mkdir', 'cd', 'mv', 'rm', 'touch', 'echo', 'cat', 'pull', 'push', 'xcat', 'lg' ] });
